import { Model, Document, Types } from 'mongoose';
import { PageData } from '../common/pageData';

/**
 * @class BaseService
 */
export abstract class BaseService<T> {

    abstract repository: Model<T & Document>;

    /**
     * @description find by conditions
     * @param {any} conditions
     * @param {any} projection
     * @param {any} sort
     * @param {any} options
     * @returns {Promise<T[]>}
     */
    async find(conditions: any, projection?: any, sort?: any, options?: any): Promise<T[]> {
        conditions['isDeleted'] = { $ne: 1 };
        return await this.repository.find(conditions, projection, options).sort(sort).lean() as T[];
    }

    /**
     * @description find by conditions
     * @param {any} conditions
     * @param {any} projection
     * @param {any} options
     * @returns {Promise<T>}
     */
    async findOne(conditions: any, projection?: any, options?: any): Promise<T> {
        conditions['isDeleted'] = { $ne: 1 };
        return await this.repository.findOne(conditions, projection, options);
    }

    /**
     * @description Fetches all docs from the storage
     * @param {any} projection
     * @param {any} sort
     * @returns {Promise<T[]>}
     */
    async findAll(projection?: any, sort?: any): Promise<T[]> {
        const conditions = { isDeleted: { $ne: 1 } };
        return await this.repository.find(conditions, projection).sort(sort).lean() as T[];
    }

    /**
     * @description Count documents
     * @param {any} conditions
     * @returns {Promise<number>}
     */
    async countDocuments(conditions: any): Promise<number> {
        conditions['isDeleted'] = { $ne: 1 };
        return await this.repository.countDocuments(conditions);
    }

    /**
     * @description Fetches multiple docs from the storage by page
     * @param {any} conditions
     * @param {number} page default 1
     * @param {number} pageSize default 10
     * @param {any} projection
     * @param {any} sort
     * @returns {Promise<PageData<T>>}
     */
    async findByPage(conditions: any, page: number = 1, pageSize: number = 10, projection?: any, sort?: any): Promise<PageData<T>> {
        conditions['isDeleted'] = { $ne: 1 };
        const pageData = new PageData<T>(page, pageSize);
        pageData.total = await this.repository.countDocuments(conditions);
        pageData.dataList = await this.repository.find(conditions, projection).limit(pageSize).skip((page - 1) * pageSize).sort(sort).lean() as T[];
        pageData.hasMore = pageData.total - (page - 1) * pageSize > pageSize ? true : false;
        return pageData;
    }

    /**
     * @description Fetches multiple docs from the storage
     * @param {any} conditions
     * @returns {Promise<any[]>}
     */
    async aggregate(aggregations: any[]): Promise<any[]> {
        if (aggregations[0].$match) aggregations[0].$match.isDeleted = { $ne: 1 };
        else aggregations.unshift({ $match: { isDeleted: { $ne: 1 } } });

        return await this.repository.aggregate(aggregations);
    }

    /**
     * @description Fetches multiple docs from the storage by page
     * @param {any[]} aggregations
     * @param {number} page default 1
     * @param {number} pageSize default 10
     * @returns {Promise<PageData<T>>}
     */
    async aggregateByPage(aggregations: any[], page: number = 1, pageSize: number = 10): Promise<PageData<T>> {
        if (aggregations[0].$match) aggregations[0].$match.isDeleted = { $ne: 1 };
        else aggregations.unshift({ $match: { isDeleted: { $ne: 1 } } });
        const pageData = new PageData<T>(page, pageSize);

        // aggregate count
        aggregations.push({ $count: 'count' });
        const countArr = await this.repository.aggregate(aggregations);
        pageData.total = countArr[0] ? countArr[0].count : 0;

        // aggregate data
        aggregations.pop();
        aggregations.push({ $skip: pageSize * (page - 1) });
        aggregations.push({ $limit: pageSize });
        pageData.dataList = await this.repository.aggregate(aggregations) as T[];

        pageData.hasMore = pageData.total - (page - 1) * pageSize > pageSize ? true : false;
        return pageData;
    }

    /**
     * @description Fetches single doc from the storage by id
     * @param {string} id ObjectId/custom id
     * @param {any} projection
     * @returns {Promise<T>}
     */
    async findById(id: string, projection?: any): Promise<T> {
        const conditions = { isDeleted: { $ne: 1 } };
        conditions[this.repository.modelName.toLowerCase() + 'Id'] = id;

        let res: T & Document;
        if (!Types.ObjectId.isValid(id)) {
            res = await this.repository.findOne(conditions, projection);
        } else {
            res = await this.repository.findOne({ $or: [{ _id: id }, conditions] }, projection);
        }
        return res;
    }

    /**
     * @description Saves the doc to the storage
     * @param {T} doc
     * @returns {Promise<T>}
     */
    async save(doc: T): Promise<T> {
        return (await new this.repository(doc).save()).toObject({ virtuals: true });
    }

    /**
     * @description Saves docs to the storage
     * @param {T[]} docs
     * @returns {Promise<T>}
     */
    async saveMany(docs: T[]): Promise<T[]> {
        return await this.repository.create(docs);
    }

    /**
     * @description Fetches single doc by conditions and sets values
     * @param {T} doc
     * @returns {Promise<T>}
     */
    async findOneAndUpdate(doc: T): Promise<T> {
        const conditions = {};
        if ((doc as any)._id) {
            conditions['_id'] = (doc as any)._id;
        } else {
            const fieldName = this.repository.modelName.toLowerCase() + 'Id';
            conditions[fieldName] = (doc as any)[fieldName];
        }

        return await this.repository.findOneAndUpdate(conditions, doc, { new: true }).lean();
    }

    /**
     * @description Updates a document in the database without returning them
     * @param {any} conditions
     * @param {any} doc
     * @returns {Promise<object>}
     */
    async updateOne(conditions: any, doc: any): Promise<object> {
        return await this.repository.updateOne(conditions, doc);
    }

    /**
     * @description Deletes a single doc from storage
     * @param {string} id ObjectId/custom id
     * @returns {Promise<object>}
     */
    async deleteOne(id: string): Promise<object> {
        const conditions = {};
        conditions[this.repository.modelName.toLowerCase() + 'Id'] = id;

        if (!Types.ObjectId.isValid(id)) {
            return await this.repository.deleteOne(conditions);
        } else {
            return await this.repository.deleteOne({ $or: [{ _id: id }, conditions] });
        }
    }

    /**
     * @description Soft delete a single doc from storage
     * @param {string} id ObjectId/custom id
     * @returns {Promise<object>}
     */
    async softDeleteOne(id: string): Promise<object> {
        const conditions = { isDeleted: { $ne: 1 } };
        conditions[this.repository.modelName.toLowerCase() + 'Id'] = id;

        if (!Types.ObjectId.isValid(id)) {
            return await this.repository.updateOne(conditions, { isDeleted: 1 });
        } else {
            return await this.repository.updateOne({ $or: [{ _id: id }, conditions] }, { isDeleted: 1 });
        }
    }

    /**
     * @description Deletes multiple docs from storage
     * @param {string} ids custom ids
     * @returns {Promise<object>}
     */
    async deleteMany(ids: string[]): Promise<object> {
        const conditions = {};
        conditions[this.repository.modelName.toLowerCase() + 'Id'] = { $in: ids };

        return await this.repository.deleteMany(conditions);
    }

    /**
     * @description Soft delete multiple docs from storage
     * @param {string} ids custom ids
     * @returns {Promise<object>}
     */
    async softDeleteMany(ids: string[]): Promise<object> {
        const conditions = { isDeleted: { $ne: 1 } };
        conditions[this.repository.modelName.toLowerCase() + 'Id'] = { $in: ids };

        return await this.repository.updateMany(conditions, { isDeleted: 1 });
    }

    /**
     * @description Updates documents in the database without returning them
     * @param {any} conditions
     * @param {any} doc
     * @returns {Promise<object>}
     */
    async updateMany(conditions: any, doc: any): Promise<object> {
        conditions['isDeleted'] = { $ne: 1 };
        return await this.repository.updateMany(conditions, doc);
    }

    /**
     * @description Updates documents in the database without returning them
     * @param {any} conditions
     * @param {any} doc
     * @param {any} options
     * @returns {Promise<object>}
     */
    async update(conditions: any, doc: any, options: any): Promise<object> {
        conditions['isDeleted'] = { $ne: 1 };
        return await this.repository.update(conditions, doc, options);
    }
}